package data

import (
	"context"
	"errors"
	"fmt"
	"github.com/duke-git/lancet/convertor"
	"github.com/go-redis/redis/v8"
	jsoniter "github.com/json-iterator/go"
	"gorm.io/gorm"
	"sync"
	"time"

	"hellorockscache/internal/biz"

	"github.com/go-kratos/kratos/v2/log"
)

const (
	redisTestKey = "redis_test_key"

	redisTestFmtKey = "redis_test:%v"
)

type greeterRepo struct {
	data *Data
	log  *log.Helper
}

// NewGreeterRepo .
func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {
	return &greeterRepo{
		data: data,
		log:  log.NewHelper(logger),
	}
}

// 正确使用rockscache的方案,需要注意对"防穿透"的不同处理思路
func (r *greeterRepo) TestRocks(ctx context.Context, id int64) (*biz.Whw, error) {
	db := r.data.Mysql.Table(biz.TableName).WithContext(ctx)

	// Notice 使用 rockscache 查询数据～
	cacheKey := fmt.Sprintf(redisTestFmtKey, id)
	value, errValue := r.data.RocksCache.Fetch2(ctx, cacheKey, time.Second*600, func() (string, error) {

		model := biz.Whw{Id: id}
		// 模拟缓存过期的情况：从数据库中获取数据
		// Notice 注意不是所有的查询都会返回 recordNotFound!
		// 详见博客: https://www.cnblogs.com/paulwhw/p/17563268.html
		errTake := db.Take(&model).Error
		fmt.Println("查数据库了～～～～～～～～～～～～～～～～～～～～～～")
		// 防止缓存穿透策略
		if errTake == gorm.ErrRecordNotFound {
			return "", nil
		}

		if errTake != nil {
			return "", errTake
		}

		// Marshal
		maStr, errMa := jsoniter.MarshalToString(&model)
		if errMa != nil {
			return "", errMa
		}

		return maStr, nil
	})

	if errValue != nil {
		return nil, errValue
	}

	ret := &biz.Whw{}

	// Notice 如果上面的方法返回一个空字符串表示数据库中没有要查询的记录并且rockscache做了"防穿透"机制～以后再拿这个id查的话不会再走数据库而是直接走缓存
	// 下面有2种处理思路:
	// 1、遇到空字符串往上层抛 gorm.ErrRecordNotFound 这个错误
	//	  上层调用方如果需要对"空数据"做校验的话需要判断一下返回的error中是否携带"record not found"这个字符串,携带的话表示数据库不存在这个错误
	//    ———— 这种方案在多服务的时候,链路trace会有很多Error,不是很友好～
	// 2、遇到空字符串不往上层抛 gorm.ErrRecordNotFound 这个错误 而是返回一个"空结构体数据"
	//    上层需要对查询到的数据做一下校验: 找一个一定不为0的字段属性(比如id)，判断他是否为0，如果为0的话就表示数据库中没有这个字段～

	if value == "" {
		// 1、数据库没数据直接返回错误
		//return nil, gorm.ErrRecordNotFound

		// 2、数据库没数据返回空结构体数据～
		return ret, nil
	}

	// 有数据反序列化一下
	// Notice 注意 jsoniter.Unmarshal方法不能反序列化一个空字符串，一定要在上面先判下空 再反序列化～
	errUnmarshal := jsoniter.UnmarshalFromString(value, ret)
	if errUnmarshal != nil {
		return nil, errUnmarshal
	}

	return ret, nil
}

// Notice 不更新数据的情况，缓存过期时在高并发场景下会有"击穿"
func (r *greeterRepo) TestRocks1(ctx context.Context, id int64) error {

	db := r.data.Mysql.Table(biz.TableName).WithContext(ctx)

	model := biz.Whw{Id: id}

	// 1、缓存过期的情况：从数据库中获取数据 Notice 高并发场景下会有"击穿"
	errFirst := db.First(&model).Error
	if errFirst != nil {
		return errFirst
	}

	// 2、更新到redis
	r.data.RedisCli.Set(ctx, redisTestKey, convertor.ToString(model.Count), time.Second*100)

	return nil
}

// Notice 考虑下需要更新数据的情况！
func (r *greeterRepo) TestRocks2(ctx context.Context, id, change int64) error {

	db := r.data.Mysql.Table(biz.TableName).WithContext(ctx)

	model := biz.Whw{}

	// 1、缓存过期的情况：从数据库中获取数据 Notice 高并发场景下会有"击穿"
	errFirst := db.First(&model).Error
	if errFirst != nil {
		return errFirst
	}

	// 2、更新到redis Notice 高并发情况下会出问题!!!会出现缓存与DB不一致的情况！
	// 因为：1、并发情况下所有协程从DB中获取的初始值是一样的，+操作后的结果跟实际预想的不一样 2、 += 操作不是原子的！
	r.data.RedisCli.Set(ctx, redisTestKey, convertor.ToString(model.Count+change), time.Second*100)

	// 3、更新数据库数据 Notice 这一步是并发安全的
	err := db.Where("id = ?", id).Update("count", gorm.Expr("count + ?", change)).Error
	if err != nil {
		return err
	}

	return nil
}

func (r *greeterRepo) TestPipeline(ctx context.Context) error {
	setKeyFmtStr := "login%v"

	cts := time.Now().Format("20060102")
	ctsYesterday := time.Now().Add(-time.Hour * 24).Format("20060102")

	setKey1 := fmt.Sprintf(setKeyFmtStr, cts)
	setKey2 := fmt.Sprintf(setKeyFmtStr, ctsYesterday)

	cacheMemberKey := fmt.Sprintf("%v:%v", 100, 2023)

	// 使用 pipeline
	pipe := r.data.RedisCli.Pipeline()
	keyLst := []string{setKey1, setKey2}
	fmt.Println("keyLst: ", keyLst)

	for _, setKey := range keyLst {
		//currKey := setKey
		pipe.ZScore(ctx, setKey, cacheMemberKey)
	}
	cmders, errExec := pipe.Exec(ctx)
	// Notice 注意 redis.Nil 错误
	if errExec != nil && errExec != redis.Nil {
		return errors.New(fmt.Sprintf("errExec: %v", errExec))
	}

	var loginBit []int
	for _, cmder := range cmders {
		cmd := cmder.(*redis.FloatCmd)
		// Notice 注意 redis.Nil 错误
		dest, errDest := cmd.Result()
		if errDest != nil && errDest != redis.Nil {
			fmt.Println("errDest: ", errDest)
		} else {
			loginBit = append(loginBit, int(dest))
		}
	}

	fmt.Println("pipeline执行后loginBit的结果为: >>> ", loginBit)

	return nil
}

func (r *greeterRepo) TestSetnx(ctx context.Context) error {

	// 测试 setnx
	itemLst := []int{1, 2, 3, 4, 5, 6, 7}
	key := fmt.Sprintf("testRocksKey:123")
	wg := sync.WaitGroup{}

	for _, item := range itemLst {
		//currItem := item
		wg.Add(1)

		go func(currItem int) {
			defer wg.Done()
			ret, err := r.data.RedisCli.SetNX(ctx, key, true, time.Hour).Result()
			if err != nil {
				fmt.Printf(">>>>>>>err: %v, item: %v \n", err, currItem)
			}
			fmt.Printf(">>>>>> ret: %v, item: %v \n", ret, currItem)
		}(item) // Notice 这样传也可以防止 惰性计算
	}

	wg.Wait()

	// 缓存一秒
	errExpire := r.data.RedisCli.Expire(ctx, key, time.Second).Err()
	if errExpire != nil {
		return errExpire
	}

	return nil
}

const (
	mutDealKey = "test_multi_click:"
)

// MultiCheck 防并发处理,非重试的分布式锁
// Notice uniqueLockKey 需要业务调用方自己生成!最好做到业务+用户维度,不要有不相关的业务与用户出现同一个uniqueKey的情况
func (dc *greeterRepo) MultiCheck(ctx context.Context, uniqueLockKey string, lockTime time.Duration) (result bool) {

	cacheKey := mutDealKey + uniqueLockKey
	result, err := dc.data.RedisCli.SetNX(ctx, cacheKey, 1, lockTime).Result()
	if err != nil {
		return false
	}

	return true
}
